﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.ServiceModel;

namespace ExpressionSerialization
{
    public abstract class ClientBase<TChannel> where TChannel : IQueryService
    {

        public ClientBase(Uri baseAddress, IEnumerable<Type> @knownTypes = null)
		{
		    BaseAddress = baseAddress;
		    _knownTypes = @knownTypes == null ? 
                new HashSet<Type>(GetServiceKnownTypes(typeof(TChannel))) 
                : new HashSet<Type>(GetServiceKnownTypes(typeof(TChannel)).Union(@knownTypes));
		}

        
        protected HashSet<Type> _knownTypes;
        public IEnumerable<Type> KnownTypes { get { return _knownTypes.AsEnumerable(); } }
        public Uri BaseAddress { get; protected set; }

        protected static IEnumerable<Type> GetServiceKnownTypes(Type service)
        {
            HashSet<Type> knownTypes = new HashSet<Type>();
            object[] customattributes = service.GetCustomAttributes(true);
            IEnumerable<ServiceKnownTypeAttribute> kattrs = customattributes.OfType<ServiceKnownTypeAttribute>();
            foreach (var k in kattrs)
                knownTypes.Add(k.Type);

            MethodInfo[] methods = service.GetMethods();
            foreach (var m in methods)
            {
                foreach (var k in m.GetCustomAttributes(true).OfType<ServiceKnownTypeAttribute>())
                    knownTypes.Add(k.Type);
            }
            return knownTypes;
        }

        /// <summary>
        /// Specifying the returnType is necessary because sometimes DataContractSerializer cannot deserialize the response
        /// without knowing the exact Type of the response.
        /// Sometimes DCS fails to deserialize, sometimes it doesn't, but the best practice is to always specify the expected return Type.
        /// 
        /// If calling from SL, this call must be made from a thread separate from the main UI thread. For example,
        /// enclose this call within a call to ThreadPool.QueueUserWorkItem.
        /// </summary>
        /// <typeparam name="TResult"></typeparam>
        /// <param name="methodcall">Lambda Expression that is the call to a method on the WebHttp service.</param>
        /// <param name="returnType">expected Type returned in the response</param>
        /// <returns></returns>
        //public object SynchronousCall<TResult>(Expression<Func<TChannel, TResult>> methodcall, Type returnType = null)
        public abstract object SynchronousCall(Expression<Func<TChannel, object>> methodcall, Type returnType);

        public TResult SynchronousCall<TResult>(Expression<Func<TChannel, object>> methodcall)
        {
            return (TResult)this.SynchronousCall(methodcall: methodcall, returnType: typeof(TResult));
        }

        protected static dynamic GetParameters(MethodCallExpression m)
        {
            if (m.Arguments.Count == 0)
                return null;
            Expression argexp = m.Arguments[0];
            var evald = (ConstantExpression)Evaluator.PartialEval(argexp);
            LambdaExpression lambda = Expression.Lambda(evald);
            dynamic args = lambda.Compile().DynamicInvoke(new object[0]);
            return args;
        }
    }
}